package cn.edu.dgut.sw.lab.security.oauth2.client;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.nimbusds.oauth2.sdk.*;
import com.nimbusds.oauth2.sdk.auth.ClientAuthentication;
import com.nimbusds.oauth2.sdk.auth.ClientSecretBasic;
import com.nimbusds.oauth2.sdk.auth.ClientSecretPost;
import com.nimbusds.oauth2.sdk.auth.Secret;
import com.nimbusds.oauth2.sdk.http.HTTPRequest;
import com.nimbusds.oauth2.sdk.http.HTTPResponse;
import com.nimbusds.oauth2.sdk.id.ClientID;
import net.minidev.json.JSONObject;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.core.*;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
import org.springframework.util.CollectionUtils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;

import java.io.IOException;
import java.net.URI;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;

@Deprecated
public class QQNimbusAuthorizationCodeTokenResponseClient implements OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> {
    private static final String INVALID_TOKEN_RESPONSE_ERROR_CODE = "invalid_token_response";

    @SuppressWarnings("Duplicates")
    @Override
    public OAuth2AccessTokenResponse getTokenResponse(OAuth2AuthorizationCodeGrantRequest authorizationGrantRequest)
            throws OAuth2AuthenticationException {

        ClientRegistration clientRegistration = authorizationGrantRequest.getClientRegistration();

        // Build the authorization code grant request for the token endpoint
        AuthorizationCode authorizationCode = new AuthorizationCode(
                authorizationGrantRequest.getAuthorizationExchange().getAuthorizationResponse().getCode());
        URI redirectUri = toURI(authorizationGrantRequest.getAuthorizationExchange().getAuthorizationRequest().getRedirectUri());
        AuthorizationGrant authorizationCodeGrant = new AuthorizationCodeGrant(authorizationCode, redirectUri);
        URI tokenUri = toURI(clientRegistration.getProviderDetails().getTokenUri());

        // Set the credentials to authenticate the client at the token endpoint
        ClientID clientId = new ClientID(clientRegistration.getClientId());
        Secret clientSecret = new Secret(clientRegistration.getClientSecret());
        ClientAuthentication clientAuthentication;
        if (ClientAuthenticationMethod.POST.equals(clientRegistration.getClientAuthenticationMethod())) {
            clientAuthentication = new ClientSecretPost(clientId, clientSecret);
        } else {
            clientAuthentication = new ClientSecretBasic(clientId, clientSecret);
        }

        com.nimbusds.oauth2.sdk.TokenResponse tokenResponse;
        try {
            // Send the Access Token request
            TokenRequest tokenRequest = new TokenRequest(tokenUri, clientAuthentication, authorizationCodeGrant);
            HTTPRequest httpRequest = tokenRequest.toHTTPRequest();
            httpRequest.setAccept(MediaType.APPLICATION_JSON_VALUE);
            httpRequest.setConnectTimeout(30000);
            httpRequest.setReadTimeout(30000);

            HTTPResponse httpResponse = httpRequest.send();

            // QQ的token接口返回的response的contentType为text/html,需要修改为application/josn，否则会抛异常。
            httpResponse.setContentType(MediaType.APPLICATION_JSON_VALUE);
            httpResponse.ensureStatusCode(HTTPResponse.SC_OK);

            // body数据为form的格式,先转为map,map再转为json字符串，最后构造新的body内容
            String content = httpResponse.getContent();
            String[] pairs = StringUtils.tokenizeToStringArray(content, "&");
            MultiValueMap<String, String> result = new LinkedMultiValueMap<>(pairs.length);
            for (String pair : pairs) {
                int idx = pair.indexOf('=');
                if (idx == -1) {
                    result.add(pair, null);
                } else {
                    result.add(pair.substring(0, idx), pair.substring(idx + 1));
                }
            }
            httpResponse.setContent(JSONObject.toJSONString(result.toSingleValueMap()));

            // 构造tokenResponse
            JSONObject jsonObject = httpResponse.getContentAsJSONObject();
            // QQ的token接口返回的json中没有token_type,需要增加一个，否则会抛异常。
            jsonObject.appendField("token_type", "Bearer");
            tokenResponse = com.nimbusds.oauth2.sdk.TokenResponse.parse(jsonObject);

        } catch (ParseException pe) {
            org.springframework.security.oauth2.core.OAuth2Error oauth2Error = new org.springframework.security.oauth2.core.OAuth2Error(INVALID_TOKEN_RESPONSE_ERROR_CODE,
                    "An error occurred parsing the Access Token response: " + pe.getMessage(), null);
            throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString(), pe);
        } catch (IOException ioe) {
            throw new AuthenticationServiceException("An error occurred while sending the Access Token Request: " +
                    ioe.getMessage(), ioe);
        }

        if (!tokenResponse.indicatesSuccess()) {
            TokenErrorResponse tokenErrorResponse = (TokenErrorResponse) tokenResponse;
            ErrorObject errorObject = tokenErrorResponse.getErrorObject();
            org.springframework.security.oauth2.core.OAuth2Error oauth2Error;
            if (errorObject == null) {
                oauth2Error = new org.springframework.security.oauth2.core.OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR);
            } else {
                oauth2Error = new OAuth2Error(
                        errorObject.getCode() != null ? errorObject.getCode() : OAuth2ErrorCodes.SERVER_ERROR,
                        errorObject.getDescription(),
                        errorObject.getURI() != null ? errorObject.getURI().toString() : null);
            }
            throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
        }

        AccessTokenResponse accessTokenResponse = (AccessTokenResponse) tokenResponse;

        // QQ的token接口返回信息中没有openid,要通过另外的接口获取
        String openid;
        try {
            HTTPRequest openidRquest = new HTTPRequest(HTTPRequest.Method.GET, toURI("https://graph.qq.com/oauth2.0/me").toURL());
            openidRquest.setQuery("access_token=" + accessTokenResponse.getTokens().getAccessToken().getValue());
            openidRquest.setConnectTimeout(30000);
            openidRquest.setReadTimeout(30000);

            HTTPResponse openidResponse = openidRquest.send();
            // callback( {"client_id":"YOUR_APPID","openid":"YOUR_OPENID"} );
            // 截取json字符串
            String openidJsonStr = "{" + org.thymeleaf.util.StringUtils.substringBefore(org.thymeleaf.util.StringUtils.substringAfter(openidResponse.getContent(), "{"), "}") + "}";
            // 把返回的json字符串转换为map<String,?>
            ObjectMapper objectMapper = new ObjectMapper();
            Map<String, ?> map = objectMapper.readValue(openidJsonStr, new TypeReference<Map<String, ?>>() {
            });

            if (!map.containsKey("openid") || map.containsKey("error") || map.get("openid") == null)
                throw new IllegalArgumentException("Could not get the QQ openid:" + map.get("error"));

            openid = map.get("openid").toString();

        } catch (IOException e) {
            throw new AuthenticationServiceException("An error occurred while getting the Openid Request: " +
                    e.getMessage(), e);
        }

        String accessToken = accessTokenResponse.getTokens().getAccessToken().getValue();
        OAuth2AccessToken.TokenType accessTokenType = null;
        if (OAuth2AccessToken.TokenType.BEARER.getValue().equalsIgnoreCase(accessTokenResponse.getTokens().getAccessToken().getType().getValue())) {
            accessTokenType = OAuth2AccessToken.TokenType.BEARER;
        }
        long expiresIn = accessTokenResponse.getTokens().getAccessToken().getLifetime();

        // As per spec, in section 5.1 Successful Access Token Response
        // https://tools.ietf.org/html/rfc6749#section-5.1
        // If AccessTokenResponse.scope is empty, then default to the scope
        // originally requested by the client in the Authorization Request
        Set<String> scopes;
        if (CollectionUtils.isEmpty(accessTokenResponse.getTokens().getAccessToken().getScope())) {
            scopes = new LinkedHashSet<>(
                    authorizationGrantRequest.getAuthorizationExchange().getAuthorizationRequest().getScopes());
        } else {
            scopes = new LinkedHashSet<>(
                    accessTokenResponse.getTokens().getAccessToken().getScope().toStringList());
        }

        String refreshToken = null;
        if (accessTokenResponse.getTokens().getRefreshToken() != null) {
            refreshToken = accessTokenResponse.getTokens().getRefreshToken().getValue();
        }

        Map<String, Object> additionalParameters = new LinkedHashMap<>(accessTokenResponse.getCustomParameters());
        additionalParameters.put("openid", openid);

        return OAuth2AccessTokenResponse.withToken(accessToken)
                .tokenType(accessTokenType)
                .expiresIn(expiresIn)
                .scopes(scopes)
                .refreshToken(refreshToken)
                .additionalParameters(additionalParameters)
                .build();
    }

    private static URI toURI(String uriStr) {
        try {
            return new URI(uriStr);
        } catch (Exception ex) {
            throw new IllegalArgumentException("An error occurred parsing URI: " + uriStr, ex);
        }
    }
}
